دليل شامل لأنواع TypeScript العامة (generics)، يغطي صيغتها ومزاياها واستخداماتها المتقدمة وأفضل الممارسات للتعامل مع أنواع البيانات المعقدة في تطوير البرمجيات العالمية.
أنواع TypeScript العامة (Generics): إتقان أنواع البيانات المعقدة للتطبيقات القوية
لغة TypeScript، وهي مجموعة شاملة من JavaScript، تمكن المطورين من كتابة كود أكثر قوة وقابلية للصيانة من خلال الكتابة الثابتة (static typing). من بين أقوى ميزاتها هي الأنواع العامة (generics)، والتي تسمح لك بكتابة كود يمكنه العمل مع مجموعة متنوعة من أنواع البيانات مع الحفاظ على أمان الأنواع. يقدم هذا الدليل استكشافًا شاملًا لأنواع TypeScript العامة، مع التركيز على تطبيقها على أنواع البيانات المعقدة في سياق تطوير البرمجيات العالمية.
ما هي الأنواع العامة (Generics)؟
توفر الأنواع العامة طريقة لكتابة كود قابل لإعادة الاستخدام يمكنه العمل مع أنواع مختلفة. بدلاً من كتابة دوال أو فئات منفصلة لكل نوع تريد دعمه، يمكنك كتابة دالة أو فئة واحدة تستخدم معاملات النوع. هذه المعاملات هي بمثابة عناصر نائبة للأنواع الفعلية التي سيتم استخدامها عند استدعاء الدالة أو إنشاء مثيل من الفئة. هذا مفيد بشكل خاص عند التعامل مع هياكل البيانات المعقدة حيث قد يختلف نوع البيانات داخل تلك الهياكل.
فوائد استخدام الأنواع العامة
- إعادة استخدام الكود: اكتب الكود مرة واحدة واستخدمه مع أنواع مختلفة. هذا يقلل من تكرار الكود ويجعل قاعدة الكود الخاصة بك أكثر قابلية للصيانة.
- أمان الأنواع: تسمح الأنواع العامة لمترجم TypeScript بفرض أمان الأنواع في وقت الترجمة. هذا يساعد على منع أخطاء وقت التشغيل المتعلقة بعدم تطابق الأنواع.
- تحسين القراءة: تجعل الأنواع العامة الكود الخاص بك أكثر قابلية للقراءة من خلال الإشارة بوضوح إلى الأنواع التي تم تصميم دوالك وفئاتك للعمل معها.
- أداء محسّن: في بعض الحالات، يمكن أن تؤدي الأنواع العامة إلى تحسينات في الأداء لأن المترجم يمكنه تحسين الكود المولد بناءً على الأنواع المحددة المستخدمة.
الصيغة الأساسية للأنواع العامة
تتضمن الصيغة الأساسية للأنواع العامة استخدام الأقواس الزاوية (< >) للإعلان عن معاملات النوع. عادةً ما يتم تسمية هذه المعاملات بأسماء مثل T
، K
، V
، وما إلى ذلك، ولكن يمكنك استخدام أي معرف صالح. إليك مثال بسيط لدالة عامة:
function identity<T>(arg: T): T {
return arg;
}
let myString: string = identity<string>("hello");
let myNumber: number = identity<number>(123);
let myBoolean: boolean = identity<boolean>(true);
console.log(myString); // Output: hello
console.log(myNumber); // Output: 123
console.log(myBoolean); // Output: true
في هذا المثال، <T>
يعلن عن معامل نوع يسمى T
. تأخذ الدالة identity
وسيطًا من النوع T
وتعيد قيمة من النوع T
. عند استدعاء الدالة، يمكنك تحديد معامل النوع بشكل صريح (على سبيل المثال، identity<string>
) أو ترك TypeScript يستنتجه بناءً على نوع الوسيط.
العمل مع أنواع البيانات المعقدة
تصبح الأنواع العامة ذات قيمة خاصة عند التعامل مع أنواع البيانات المعقدة مثل المصفوفات والكائنات والواجهات. دعنا نستكشف بعض السيناريوهات الشائعة:
المصفوفات العامة
يمكنك استخدام الأنواع العامة لإنشاء دوال أو فئات تعمل مع مصفوفات من أنواع مختلفة:
function arrayToString<T>(arr: T[]): string {
return arr.join(", ");
}
let numberArray: number[] = [1, 2, 3, 4, 5];
let stringArray: string[] = ["apple", "banana", "cherry"];
console.log(arrayToString(numberArray)); // Output: 1, 2, 3, 4, 5
console.log(arrayToString(stringArray)); // Output: apple, banana, cherry
هنا، تأخذ الدالة arrayToString
مصفوفة من النوع T[]
وتعيد تمثيلًا نصيًا للمصفوفة. تعمل هذه الدالة مع مصفوفات من أي نوع، مما يجعلها قابلة لإعادة الاستخدام بشكل كبير.
الكائنات العامة
يمكن أيضًا استخدام الأنواع العامة لتعريف دوال أو فئات تعمل مع كائنات ذات أشكال مختلفة:
interface Person {
name: string;
age: number;
country: string; // Added country for global context
}
interface Product {
id: number;
name: string;
price: number;
currency: string; // Added currency for global context
}
function displayInfo<T extends { name: string }>(item: T): void {
console.log(`Name: ${item.name}`);
}
let person: Person = { name: "Alice", age: 30, country: "USA" };
let product: Product = { id: 1, name: "Laptop", price: 1200, currency: "USD" };
displayInfo(person); // Output: Name: Alice
displayInfo(product); // Output: Name: Laptop
في هذا المثال، تأخذ الدالة displayInfo
كائنًا من النوع T
يجب أن يحتوي على خاصية name
من النوع string. العبارة extends { name: string }
هي قيد (constraint)، يحدد المتطلبات الدنيا لمعامل النوع T
. هذا يضمن أن الدالة يمكنها الوصول بأمان إلى خاصية name
.
الاستخدام المتقدم للأنواع العامة
تقدم أنواع TypeScript العامة ميزات أكثر تقدمًا تسمح لك بإنشاء كود أكثر مرونة وقوة. دعنا نستكشف بعضًا من هذه الميزات:
معاملات النوع المتعددة
يمكنك تعريف دوال أو فئات بمعاملات نوع متعددة:
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
interface Name {
firstName: string;
}
interface Age {
age: number;
}
const person: Name = { firstName: "Bob" };
const details: Age = { age: 42 };
const merged = merge(person, details);
console.log(merged.firstName); // Output: Bob
console.log(merged.age); // Output: 42
تأخذ الدالة merge
كائنين من النوعين T
و U
وتعيد كائنًا جديدًا يحتوي على خصائص كلا الكائنين. هذه طريقة قوية لدمج البيانات من مصادر مختلفة.
القيود على الأنواع العامة
كما هو موضح سابقًا، تسمح لك القيود بتقييد الأنواع التي يمكن استخدامها مع معامل النوع العام. هذا يضمن أن الكود العام يمكنه العمل بأمان على الأنواع المحددة.
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
loggingIdentity([1, 2, 3]); // Output: 3
loggingIdentity("hello"); // Output: 5
// loggingIdentity(123); // Error: Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
تأخذ الدالة loggingIdentity
وسيطًا من النوع T
يجب أن يحتوي على خاصية length
من النوع number. هذا يضمن أن الدالة يمكنها الوصول بأمان إلى خاصية length
.
الفئات العامة
يمكن أيضًا استخدام الأنواع العامة مع الفئات:
class DataStorage<T> {
private data: T[] = [];
addItem(item: T) {
this.data.push(item);
}
removeItem(item: T) {
this.data = this.data.filter(d => d !== item);
}
getItems(): T[] {
return [...this.data];
}
}
const textStorage = new DataStorage<string>();
textStorage.addItem("apple");
textStorage.addItem("banana");
textStorage.removeItem("apple");
console.log(textStorage.getItems()); // Output: [ 'banana' ]
const numberStorage = new DataStorage<number>();
numberStorage.addItem(1);
numberStorage.addItem(2);
numberStorage.removeItem(1);
console.log(numberStorage.getItems()); // Output: [ 2 ]
يمكن للفئة DataStorage
تخزين بيانات من أي نوع T
. هذا يسمح لك بإنشاء هياكل بيانات قابلة لإعادة الاستخدام وآمنة من حيث النوع.
الواجهات العامة
الواجهات العامة مفيدة لتعريف العقود التي يمكن أن تعمل مع أنواع مختلفة. على سبيل المثال:
interface Result<T, E> {
success: boolean;
data?: T;
error?: E;
}
interface User {
id: number;
username: string;
email: string;
}
interface ErrorMessage {
code: number;
message: string;
}
function fetchUser(id: number): Result<User, ErrorMessage> {
if (id === 1) {
return { success: true, data: { id: 1, username: "john.doe", email: "john.doe@example.com" } };
} else {
return { success: false, error: { code: 404, message: "User not found" } };
}
}
const userResult = fetchUser(1);
if (userResult.success) {
console.log(userResult.data.username);
} else {
console.log(userResult.error.message);
}
تُعرّف الواجهة Result
بنية عامة لتمثيل نتيجة عملية ما. يمكن أن تحتوي إما على بيانات من النوع T
أو خطأ من النوع E
. هذا نمط شائع للتعامل مع العمليات غير المتزامنة أو العمليات التي قد تفشل.
الأنواع المساعدة والأنواع العامة
توفر TypeScript العديد من الأنواع المساعدة المدمجة التي تعمل بشكل جيد مع الأنواع العامة. يمكن أن تساعدك هذه الأنواع المساعدة في تحويل الأنواع ومعالجتها بطرق قوية.
Partial<T>
Partial<T>
يجعل جميع خصائص النوع T
اختيارية:
interface Person {
name: string;
age: number;
}
type PartialPerson = Partial<Person>;
const partialPerson: PartialPerson = { name: "Alice" }; // Valid
Readonly<T>
Readonly<T>
يجعل جميع خصائص النوع T
للقراءة فقط:
interface Person {
name: string;
age: number;
}
type ReadonlyPerson = Readonly<Person>;
const readonlyPerson: ReadonlyPerson = { name: "Bob", age: 42 };
// readonlyPerson.age = 43; // Error: Cannot assign to 'age' because it is a read-only property.
Pick<T, K>
Pick<T, K>
يختار مجموعة من الخصائص K
من النوع T
:
interface Person {
name: string;
age: number;
email: string;
}
type NameAndAge = Pick<Person, "name" | "age">;
const nameAndAge: NameAndAge = { name: "Charlie", age: 28 };
Omit<T, K>
Omit<T, K>
يزيل مجموعة من الخصائص K
من النوع T
:
interface Person {
name: string;
age: number;
email: string;
}
type PersonWithoutEmail = Omit<Person, "email">;
const personWithoutEmail: PersonWithoutEmail = { name: "David", age: 35 };
Record<K, T>
Record<K, T>
ينشئ نوعًا بمفاتيح K
وقيم من النوع T
:
type CountryCodes = "US" | "CA" | "UK" | "DE" | "FR" | "JP" | "CN" | "IN" | "BR" | "AU"; // Expanded list for global context
type Currency = "USD" | "CAD" | "GBP" | "EUR" | "JPY" | "CNY" | "INR" | "BRL" | "AUD"; // Expanded list for global context
type CurrencyMap = Record<CountryCodes, Currency>;
const currencyMap: CurrencyMap = {
"US": "USD",
"CA": "CAD",
"UK": "GBP",
"DE": "EUR",
"FR": "EUR",
"JP": "JPY",
"CN": "CNY",
"IN": "INR",
"BR": "BRL",
"AU": "AUD",
};
الأنواع المعينة (Mapped Types)
تسمح لك الأنواع المعينة بتحويل الأنواع الموجودة عن طريق التكرار على خصائصها. هذه طريقة قوية لإنشاء أنواع جديدة بناءً على الأنواع الموجودة. على سبيل المثال، يمكنك إنشاء نوع يجعل جميع خصائص نوع آخر للقراءة فقط:
interface Person {
name: string;
age: number;
}
type ReadonlyPerson = {
readonly [K in keyof Person]: Person[K];
};
const readonlyPerson: ReadonlyPerson = { name: "Eve", age: 25 };
// readonlyPerson.age = 26; // Error: Cannot assign to 'age' because it is a read-only property.
في هذا المثال، [K in keyof Person]
يتكرر على جميع مفاتيح الواجهة Person
، و Person[K]
يصل إلى نوع كل خاصية. الكلمة المفتاحية readonly
تجعل كل خاصية للقراءة فقط.
الأنواع الشرطية (Conditional Types)
تسمح لك الأنواع الشرطية بتعريف أنواع بناءً على شروط. هذه طريقة قوية لإنشاء أنواع تتكيف مع سيناريوهات مختلفة.
type NonNullable<T> = T extends null | undefined ? never : T;
type MaybeString = string | null | undefined;
type StringType = NonNullable<MaybeString>; // string
function getValue<T>(value: T): NonNullable<T> {
if (value == null) { // Handles both null and undefined
throw new Error("Value cannot be null or undefined");
}
return value as NonNullable<T>;
}
try {
const validValue = getValue("hello");
console.log(validValue.toUpperCase()); // Output: HELLO
const invalidValue = getValue(null); // This will throw an error
console.log(invalidValue); // This line will not be reached
} catch (error: any) {
console.error(error.message); // Output: Value cannot be null or undefined
}
في هذا المثال، يتحقق النوع NonNullable<T>
مما إذا كان T
هو null
أو undefined
. إذا كان كذلك، فإنه يعيد never
، مما يعني أن النوع غير مسموح به. وإلا، فإنه يعيد T
. هذا يسمح لك بإنشاء أنواع مضمونة بأنها غير قابلة للقيم الفارغة.
أفضل الممارسات لاستخدام الأنواع العامة
فيما يلي بعض أفضل الممارسات التي يجب مراعاتها عند استخدام الأنواع العامة:
- استخدم أسماء معاملات نوع وصفية: اختر أسماء تشير بوضوح إلى الغرض من معامل النوع.
- استخدم القيود للحد من الأنواع التي يمكن استخدامها مع معامل النوع العام: هذا يضمن أن الكود العام الخاص بك يمكنه العمل بأمان على الأنواع المحددة.
- اجعل الكود العام الخاص بك بسيطًا ومركزًا: تجنب تعقيد الكود العام الخاص بك بعدد كبير جدًا من معاملات النوع أو القيود المعقدة.
- وثّق الكود العام الخاص بك جيدًا: اشرح الغرض من معاملات النوع وأي قيود مستخدمة.
- ضع في اعتبارك المفاضلات بين إعادة استخدام الكود وأمان الأنواع: بينما يمكن للأنواع العامة تحسين إعادة استخدام الكود، إلا أنها يمكن أن تجعل الكود الخاص بك أكثر تعقيدًا. وازن بين الفوائد والعيوب قبل استخدام الأنواع العامة.
- مراعاة التوطين والعولمة (l10n و g11n): عند التعامل مع البيانات التي تحتاج إلى عرضها للمستخدمين في مناطق مختلفة، تأكد من أن الأنواع العامة تدعم التنسيقات والتقاليد الثقافية المناسبة. على سبيل المثال، يمكن أن يختلف تنسيق الأرقام والتواريخ بشكل كبير بين اللغات والثقافات المختلفة.
أمثلة في سياق عالمي
دعنا ننظر في بعض الأمثلة على كيفية استخدام الأنواع العامة في سياق عالمي:
تحويل العملات
interface ConversionRate {
rate: number;
fromCurrency: string;
toCurrency: string;
}
function convertCurrency<T extends ConversionRate>(amount: number, rate: T): number {
return amount * rate.rate;
}
const usdToEurRate: ConversionRate = { rate: 0.85, fromCurrency: "USD", toCurrency: "EUR" };
const amountInUSD = 100;
const amountInEUR = convertCurrency(amountInUSD, usdToEurRate);
console.log(`${amountInUSD} USD is equal to ${amountInEUR} EUR`); // Output: 100 USD is equal to 85 EUR
تنسيق التاريخ
interface DateFormatOptions {
locale: string;
options: Intl.DateTimeFormatOptions;
}
function formatDate<T extends DateFormatOptions>(date: Date, format: T): string {
return date.toLocaleDateString(format.locale, format.options);
}
const currentDate = new Date();
const usDateFormat: DateFormatOptions = { locale: "en-US", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const germanDateFormat: DateFormatOptions = { locale: "de-DE", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const japaneseDateFormat: DateFormatOptions = { locale: "ja-JP", options: { year: 'numeric', month: 'long', day: 'numeric' } };
console.log("US Date: " + formatDate(currentDate, usDateFormat));
console.log("German Date: " + formatDate(currentDate, germanDateFormat));
console.log("Japanese Date: " + formatDate(currentDate, japaneseDateFormat));
خدمة الترجمة
interface Translation {
[key: string]: string; // Allows for dynamic language keys
}
interface LanguageData<T extends Translation> {
languageCode: string;
translations: T;
}
const englishTranslations: Translation = {
"hello": "Hello",
"goodbye": "Goodbye",
"welcome": "Welcome to our website!"
};
const spanishTranslations: Translation = {
"hello": "Hola",
"goodbye": "Adiós",
"welcome": "¡Bienvenido a nuestro sitio web!"
};
const frenchTranslations: Translation = {
"hello": "Bonjour",
"goodbye": "Au revoir",
"welcome": "Bienvenue sur notre site web !"
};
const languageData: LanguageData<typeof englishTranslations>[] = [
{languageCode: "en", translations: englishTranslations },
{languageCode: "es", translations: spanishTranslations },
{languageCode: "fr", translations: frenchTranslations}
];
function translate<T extends Translation>(key: string, languageCode: string, languageData: LanguageData<T>[]): string {
const lang = languageData.find(lang => lang.languageCode === languageCode);
if (!lang) {
return `Translation for ${key} in ${languageCode} not found.`;
}
return lang.translations[key] || `Translation for ${key} not found.`;
}
console.log(translate("hello", "en", languageData)); // Output: Hello
console.log(translate("hello", "es", languageData)); // Output: Hola
console.log(translate("welcome", "fr", languageData)); // Output: Bienvenue sur notre site web !
console.log(translate("missingKey", "de", languageData)); // Output: Translation for missingKey in de not found.
الخلاصة
تعتبر أنواع TypeScript العامة أداة قوية لكتابة كود قابل لإعادة الاستخدام وآمن من حيث النوع يمكنه العمل مع أنواع البيانات المعقدة. من خلال فهم الصيغة الأساسية والميزات المتقدمة وأفضل الممارسات للأنواع العامة، يمكنك تحسين جودة وصيانة تطبيقات TypeScript الخاصة بك بشكل كبير. عند تطوير تطبيقات لجمهور عالمي، يمكن أن تساعدك الأنواع العامة في التعامل مع تنسيقات البيانات المتنوعة والتقاليد الثقافية، مما يضمن تجربة مستخدم سلسة للجميع.